package org.geektimes.web.mvc.context;

import org.geektimes.web.mvc.function.ThrowableAction;
import org.geektimes.web.mvc.function.ThrowableFunction;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import javax.naming.*;
import javax.servlet.ServletContext;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.*;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 @author :   chuan
 @date :   2021/03/10
 */
public class ComponentContext {

    private Logger logger = Logger.getLogger(CONTEXT_NAME);
    public final static String CONTEXT_NAME = ComponentContext.class.getName();
    private final static String COMPONENT_ENV_CONTEXT_NAME = "java:comp/env";

    private static ServletContext servletContext;

    private ClassLoader classLoader;

    private Context envContext;

    private Map<String, Object> componentsMap = new LinkedHashMap<>();

    public static ComponentContext getInstance(){
        return (ComponentContext) servletContext.getAttribute(CONTEXT_NAME);
    }

    public <C> C getComponent(String componentName){
        return (C) componentsMap.get(componentName);
    }

    public <C> List<C> getComponent(Class<?>... type){
        Set<String> typeSet = Arrays.stream(type).map(Class::getName).collect(Collectors.toSet());

        List<C> componentList = new LinkedList<>();
        componentsMap.values().forEach(component -> {
            Type[] interfaces = component.getClass().getInterfaces();

            for (Type anInterface : interfaces) {

                if (typeSet.contains(anInterface.getTypeName())){
                    componentList.add((C) component);

                    break;
                }
            }
        });
        return componentList;
    }

    public void init(ServletContext servletContext) {
        ComponentContext.servletContext = servletContext;
        servletContext.setAttribute(CONTEXT_NAME, this);

        this.classLoader = servletContext.getClassLoader();

        initEnvContext();
        instantiateComponents();
        initializeComponents();
    }

    protected void initializeComponents() {
        componentsMap.values().forEach(component -> {
            Class<?> componentClass = component.getClass();

            // 注入阶段 - {@link Resource}
            injectComponents(component, componentClass);

            // 初始阶段 - {@link PostConstruct}
            processPostConstruct(component, componentClass);

            // 实现销毁阶段 - {@link PreDestroy}
            processPreDestroy(component, componentClass);
        });
    }

    public void destroy() {
        componentsMap.values().forEach(component -> {
            Class<?> componentClass = component.getClass();
            doPreDestroy(component, componentClass);
        });
    }

    private void processPreDestroy(Object component, Class<?> componentClass) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> doPreDestroy(component, componentClass)));
    }

    private void doPreDestroy(Object component, Class<?> componentClass){
        Arrays.stream(componentClass.getDeclaredMethods())
                .filter(method -> {
                    int modifiers = method.getModifiers();
                    return !Modifier.isStatic(modifiers)
                            && method.getParameterCount() == 0
                            && method.isAnnotationPresent(PreDestroy.class);
                })
                .forEach(method -> {
                    try {
                        method.invoke(component);
                    } catch (Exception e) {
                    }
                });
    }

    private void processPostConstruct(Object component, Class<?> componentClass) {
        Arrays.stream(componentClass.getDeclaredMethods())
                .filter(method -> {
                    int modifiers = method.getModifiers();
                    return !Modifier.isStatic(modifiers)
                            && method.getParameterCount() == 0
                            && method.isAnnotationPresent(PostConstruct.class);
                })
                .forEach(method -> {
                    try {
                        method.invoke(component);
                    } catch (Exception e) {
                    }
                });
    }

    private void injectComponents(Object component, Class<?> componentClass) {
        Stream.of(componentClass.getDeclaredFields())
                .filter(field -> {
                    int modifiers = field.getModifiers();
                    return !Modifier.isStatic(modifiers) && field.isAnnotationPresent(Resource.class);
                })
                .forEach(field -> {
                    Resource resource = field.getAnnotation(Resource.class);
                    String componentName = resource.name();

                    Object o = lookupComponent(componentName);
                    field.setAccessible(true);

                    try {
                        field.set(component, o);
                    } catch (IllegalAccessException e) {
                    }
                });
    }

    private void instantiateComponents() {
        List<String> componentNames = listAllComponentNames();

        componentNames.forEach(componentName -> componentsMap.put(componentName, lookupComponent(componentName)));
    }

    private List<String> listAllComponentNames() {
        return listComponentNames("/");
    }

    private List<String> listComponentNames(String name) {
        return executeInContext(context -> {
            NamingEnumeration<NameClassPair> e = executeInContext(context, ctx -> ctx.list(name), false);

            if (Objects.isNull(e)){
                return Collections.emptyList();
            }

            List<String> fullNames = new LinkedList<>();
            while (e.hasMoreElements()){
                NameClassPair nameClassPair = e.nextElement();
                String className = nameClassPair.getClassName();
                Class<?> klass = this.classLoader.loadClass(className);
                if (Context.class.isAssignableFrom(klass)){
                    fullNames.addAll(listComponentNames(nameClassPair.getName()));
                }else {
                    String fullName = name.startsWith("/") ? nameClassPair.getName() : name + "/" +  nameClassPair.getName();

                    fullNames.add(fullName);
                }
            }
            return fullNames;
        });
    }

    private void initEnvContext() {
        if (Objects.nonNull(this.envContext)){
            return;
        }

        Context context = null;

        try {
            context = new InitialContext();

            this.envContext = (Context) context.lookup(COMPONENT_ENV_CONTEXT_NAME);
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }finally {
            close(context);
        }
    }

    private static void close(Context context) {
        if (Objects.nonNull(context)){
            ThrowableAction.execute(context::close);
        }
    }

    /**
     在 Context 中执行，通过指定 ThrowableFunction 返回计算结果
     *
     @param function ThrowableFunction
     @param <R>      返回结果类型
     @return 返回
     @see ThrowableFunction#apply(Object)
     */
    protected <R> R executeInContext(ThrowableFunction<Context, R> function) {
        return executeInContext(function, false);
    }

    /**
     在 Context 中执行，通过指定 ThrowableFunction 返回计算结果
     *
     @param function         ThrowableFunction
     @param ignoredException 是否忽略异常
     @param <R>              返回结果类型
     @return 返回
     @see ThrowableFunction#apply(Object)
     */
    protected <R> R executeInContext(ThrowableFunction<Context, R> function, boolean ignoredException) {
        return executeInContext(this.envContext, function, ignoredException);
    }

    private <R> R executeInContext(Context envContext, ThrowableFunction<Context, R> function, boolean ignoredException) {
        R result = null;

        try {
            result = ThrowableFunction.execute(envContext, function);
        }catch (Throwable e){
            if (ignoredException) {
                logger.warning(e.getMessage());
            } else {
                throw new RuntimeException(e);
            }
        }

        return result;
    }

    private <C> C lookupComponent(String componentName) {
        return executeInContext(context -> (C)context.lookup(componentName));
    }
}
